/*
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.xwiki.classloader.internal.protocol.jar;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FilePermission;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.security.AccessController;
import java.security.Permission;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import edu.emory.mathcs.util.classloader.ResourceUtils;
import edu.emory.mathcs.util.io.RedirectibleInput;
import edu.emory.mathcs.util.io.RedirectingInputStream;
/**
* Implementation of {@link JarURLConnection.JarOpener} that caches downloaded JAR files in a local file system.
* <p>
* Originally written by Dawid Kurzyniec and released to the public domain, as explained at
* http://creativecommons.org/licenses/publicdomain
* </p>
* <p>
* Source: http://dcl.mathcs.emory.edu/php/loadPage.php?content=util/features.html#classloading
* </p>
*
* @see JarURLConnection
* @see JarURLStreamHandler
* @version $Id: fb617dc6c4c6da749c6686f95d2be1896d850560 $
* @since 2.0.1
*/
public class JarProxy implements JarURLConnection.JarOpener
{
private final Map<URL, CachedJarFile> cache = new HashMap<URL, CachedJarFile>();
@SuppressWarnings("resource")
@Override
public JarFile openJarFile(JarURLConnection conn) throws IOException
{
URL url = conn.getJarFileURL();
CachedJarFile result;
synchronized (this.cache) {
result = this.cache.get(url);
}
if (result != null) {
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkPermission(result.perm);
}
return result;
}
// we have to download and open the JAR; yet it may be a local file
try {
URI uri = new URI(url.toString());
if (ResourceUtils.isLocalFile(uri)) {
File file = new File(uri);
Permission perm = new FilePermission(file.getAbsolutePath(), "read");
result = new CachedJarFile(file, perm, false);
}
} catch (URISyntaxException e) {
// apparently not a local file
}
if (result == null) {
final URLConnection jarconn = url.openConnection();
// set up the properties based on the JarURLConnection
jarconn.setAllowUserInteraction(conn.getAllowUserInteraction());
jarconn.setDoInput(conn.getDoInput());
jarconn.setDoOutput(conn.getDoOutput());
jarconn.setIfModifiedSince(conn.getIfModifiedSince());
Map<String, List<String>> map = conn.getRequestProperties();
for (Map.Entry<String, List<String>> entry : map.entrySet()) {
StringBuilder value = new StringBuilder();
for (String str : entry.getValue()) {
value.append(',').append(str);
}
if (value.length() >= 1) {
jarconn.setRequestProperty(entry.getKey(), value.substring(1));
}
}
jarconn.setUseCaches(conn.getUseCaches());
try (InputStream in = getJarInputStream(jarconn)) {
result = AccessController.doPrivileged(new PrivilegedExceptionAction<CachedJarFile>()
{
@Override
public CachedJarFile run() throws IOException
{
File file = File.createTempFile("jar_cache", "");
try (FileOutputStream out = new FileOutputStream(file)) {
RedirectibleInput r = new RedirectingInputStream(in, false, false);
int len = r.redirectAll(out);
out.flush();
if (len == 0) {
// e.g. HttpURLConnection: "NOT_MODIFIED"
return null;
}
}
return new CachedJarFile(file, jarconn.getPermission(), true);
}
});
} catch (PrivilegedActionException pae) {
throw (IOException) pae.getException();
}
}
// if no input came (e.g. due to NOT_MODIFIED), do not cache
if (result == null) {
return null;
}
// optimistic locking
synchronized (this.cache) {
CachedJarFile asyncResult = this.cache.get(url);
if (asyncResult != null) {
// some other thread already retrieved the file; return w/o
// security check since we already succeeded in getting past it
result.closeCachedFile();
return asyncResult;
}
this.cache.put(url, result);
return result;
}
}
protected InputStream getJarInputStream(URLConnection conn) throws IOException
{
return conn.getInputStream();
}
protected void clear()
{
Map<URL, CachedJarFile> cache;
synchronized (this.cache) {
cache = new HashMap<URL, CachedJarFile>(this.cache);
this.cache.clear();
}
for (CachedJarFile jfile : cache.values()) {
try {
jfile.closeCachedFile();
} catch (IOException e) {
// best-effort
}
}
}
@Override
protected void finalize() throws java.lang.Throwable
{
try {
clear();
} finally {
super.finalize();
}
}
private static class CachedJarFile extends JarFile
{
final Permission perm;
CachedJarFile(File file, Permission perm, boolean tmp) throws IOException
{
super(file, true, ZipFile.OPEN_READ | (tmp ? ZipFile.OPEN_DELETE : 0));
this.perm = perm;
}
@Override
public Manifest getManifest() throws IOException
{
Manifest orig = super.getManifest();
if (orig == null) {
return null;
}
// make sure the original manifest is not modified
Manifest man = new Manifest();
man.getMainAttributes().putAll(orig.getMainAttributes());
for (Map.Entry<String, Attributes> entry : orig.getEntries().entrySet()) {
man.getEntries().put(entry.getKey(), new Attributes(entry.getValue()));
}
return man;
}
@Override
public ZipEntry getEntry(String name)
{
// super.getJarEntry() would result in stack overflow
return super.getEntry(name);
}
@Override
protected void finalize() throws IOException
{
try {
closeCachedFile();
} finally {
super.finalize();
}
}
protected void closeCachedFile() throws IOException
{
super.close();
}
@Override
public void close() throws IOException
{
// no op; do NOT close file while still in cache
}
}
}